/**
 * @fileoverview
 *
 * sensor to collect ajax related events
 *
 * @author Sebastian Brzuzek sebastian@brzuzek.pl
 */

goog.provide('uxperf.profiler.SensorAjax');
goog.provide('uxperf.profiler.BufferItemAjax');
goog.provide('uxperf.profiler.SensorAjax.XMLHttpRequestEventTarget');
goog.provide('uxperf.profiler.SensorAjax.XMLHttpRequestEventTarget.EventType');
goog.provide('uxperf.profiler.SensorAjax.XDomainRequest');
goog.provide('uxperf.profiler.SensorAjax.XMLHttpRequest');
goog.provide('uxperf.profiler.SensorAjax.XMLHttpRequest.EventType');

goog.require('uxperf.profiler.SensorBase');
goog.require('uxperf.BufferItem');
goog.require('uxperf.BufferItemSensor');
goog.require('goog.events');
goog.require('goog.events.Event');
goog.require('goog.events.EventTarget');
goog.require('goog.array');
goog.require('goog.object');
goog.require('goog.net.HttpStatus');
goog.require('goog.Uri');
goog.require('goog.Uri.QueryData');

/**
 * Ajax event sensor implementation
 * NB! only one instance of ajax sensor is allowed in application
 *
 * @param {number} index number of sensor in configuration structure
 * @param {uxperf.Queue=} opt_queue optional queue to store events (global queue instance for default)
 * 
 * @constructor
 * @extends {uxperf.profiler.SensorBase}
 */
uxperf.profiler.SensorAjax = function(index, opt_queue) {
	if (uxperf.profiler.SensorAjax.instance_ != null) throw Error('Ajax sensor already defined');
	uxperf.profiler.SensorAjax.instance_ = this;
	goog.base(this, index, uxperf.profiler.SensorAjax.DEFAULT_CONFIG, opt_queue);
  if (goog.DEBUG) LOG.info('SensorAjax - construction done (sensor index=' + index + ')');
};
goog.inherits(uxperf.profiler.SensorAjax, uxperf.profiler.SensorBase);

/**
 * singleton ajax sensor instance
 * @type {uxperf.profiler.SensorAjax}
 */
uxperf.profiler.SensorAjax.instance_ = null;

/**
 * singleton getter
 * 
 * NOTE: if you will not provide index this will not instantiate new instance automatically, you have to create one using constructor
 * @param {number=} opt_index index of sensor in configuration
 * 
 * @return {uxperf.profiler.SensorAjax}
 */
uxperf.profiler.SensorAjax.getInstance = function(opt_index) {
	if (uxperf.profiler.SensorAjax.instance_) return uxperf.profiler.SensorAjax.instance_;
	if (goog.isDef(opt_index)) return new uxperf.profiler.SensorAjax(opt_index);
	return null;
};

/**
 * get parameter for ajax sensor - there can be only one instance so we don't need to provide index or instance itself
 * 
 * @param {...Array.<(number|string)>|number|string|undefined} var_args keys to point param in configuration of ajax sensor
 * @return {*}
 * @override
 */
uxperf.profiler.SensorAjax.getParam = function(var_args) {
	var inst = uxperf.profiler.SensorAjax.getInstance();
	if (!inst) return undefined;
	if ((arguments.length>1) || !goog.isArray(var_args))
		var_args = goog.array.toArray(arguments);
	return inst.getParam(var_args);
};

/**
 * Initialization of sensor
 *
 * @return {uxperf.profiler.SensorAjax} this sensor instance
 * @override
 */
uxperf.profiler.SensorAjax.prototype.init = function() {
  if (goog.DEBUG) LOG.info('SensorAjax - initialization');
	if (this.isReady()) return this;
	
	try {
    if (goog.DEBUG) LOG.info('SensorAjax - replace original XMLHttpRequest');
    //NOTE: there is assumption that all libraries are using XMLHttpRequest if available
    //      we will not try to proxy ActiveXObject
		//      if assumption is incorrect for some library sensor will not work on IE

    //save original XMLHttpRequest class and assign proxy class
    if (window.XMLHttpRequest) uxperf.profiler.SensorAjax.originalXMLHttpRequest_ = window.XMLHttpRequest;
    window.XMLHttpRequest = uxperf.profiler.SensorAjax.XMLHttpRequest;
		
		//save original XDomainRequest class if available and assign proxy class
		if (window.XDomainRequest) {
	    if (goog.DEBUG) LOG.info('SensorAjax - replace original XDomainRequest');
			uxperf.profiler.SensorAjax.originalXDomainRequest_ = window.XDomainRequest;
			window.XDomainRequest = uxperf.profiler.SensorAjax.XDomainRequest;
		}
	} catch (e) {
		this.errorNotify(e);
	}

	this.readyNotify();
  if (goog.DEBUG) LOG.info('SensorAjax - initialization done');
	return this;
};

/**
 * @inheritDoc
 */
uxperf.profiler.SensorAjax.prototype.convert = function (event) {
  if (goog.DEBUG) LOG.info('SensorAjax - convert');
	return uxperf.profiler.SensorAjax.superClass_.convert.call(
    this,
    event,
    new uxperf.BufferItemAjax( /** @type {!goog.events.Event} */ (event), this )
  );
};

/**
 * Ajax sensor event types definition
 * 
 * @enum {string}
 */
uxperf.profiler.SensorAjax.EventType = {
	BEFORE_SEND: 'before_send',
	COMPLETE: 'complete',
	SUCCESS: 'success',
	ABORT: 'abort',
	ERROR: 'error',
	ERROR_UPLOAD: 'error_upload',
	TIMEOUT: 'timeout',
	TIMEOUT_UPLOAD: 'timeout_upload'
};


/** @const ajax sensor http header prefix */
uxperf.profiler.SensorAjax.X_UXPERF_HEADER_PREFIX = 'X-Uxperf';

/** @const ajax sensor http header to store request id */
uxperf.profiler.SensorAjax.X_UXPERF_AJAX_ID_HEADER = uxperf.profiler.SensorAjax.X_UXPERF_HEADER_PREFIX + '-Ajax-ID';

/** @const key name used in reported data to indicate library error info */
uxperf.profiler.SensorAjax.ERROR_KEY = 'uxPerf-Error';


/**
 * constants for config parameters
 * 
 * @enum {string}
 */
uxperf.profiler.SensorAjax.CONFIG = {
	EXT_REQ_ID: 'ext_req_id', //value (or function which returns value) to use as part of xhrID
	TRACE_HEADERS: 'trace_headers', //flag if trace headers should be send with each request
	USE_URL_PATH: 'use_url_path', //flag if url path will be used in calculation of ext_req_id
	WITH_URL_PATH: 'with_url_path', //flag if url path should be reported
	WITH_DATA: 'with_data', //array of POST params reported, use true to report all params, false to disable
	WITH_PARAMS: 'with_params', //array of GET params reported, use true to report all params, false to disable
	WITH_RESPONSE: 'with_response', //flag if response should be reported
	WITH_STATUS_TEXT: 'with_status_text', //flag if status text should be reported
	EVENT_TYPE: 'event_type' //array of reported event types
};

/** @const sensor type name */
uxperf.profiler.SensorAjax.TYPE_NAME = 'ajax';

/**
 * Default configuration
 */
uxperf.profiler.SensorAjax.DEFAULT_CONFIG = {};
uxperf.profiler.SensorAjax.DEFAULT_CONFIG[uxperf.profiler.SensorBase.CONFIG.TYPE_KEY] = uxperf.profiler.SensorAjax.TYPE_NAME;
uxperf.profiler.SensorAjax.DEFAULT_CONFIG[uxperf.profiler.SensorBase.CONFIG.AUTOINIT_KEY] = uxperf.FactoryBase.AUTOINIT_MODE.ON_DOM_READY;
uxperf.profiler.SensorAjax.DEFAULT_CONFIG[uxperf.profiler.SensorAjax.CONFIG.EXT_REQ_ID] = '_';
uxperf.profiler.SensorAjax.DEFAULT_CONFIG[uxperf.profiler.SensorAjax.CONFIG.TRACE_HEADERS] = true;
uxperf.profiler.SensorAjax.DEFAULT_CONFIG[uxperf.profiler.SensorAjax.CONFIG.USE_URL_PATH] = false;
uxperf.profiler.SensorAjax.DEFAULT_CONFIG[uxperf.profiler.SensorAjax.CONFIG.WITH_URL_PATH] = false;
uxperf.profiler.SensorAjax.DEFAULT_CONFIG[uxperf.profiler.SensorAjax.CONFIG.WITH_DATA] = false;
uxperf.profiler.SensorAjax.DEFAULT_CONFIG[uxperf.profiler.SensorAjax.CONFIG.WITH_PARAMS] = false;
uxperf.profiler.SensorAjax.DEFAULT_CONFIG[uxperf.profiler.SensorAjax.CONFIG.WITH_RESPONSE] = false;
uxperf.profiler.SensorAjax.DEFAULT_CONFIG[uxperf.profiler.SensorAjax.CONFIG.WITH_STATUS_TEXT] = false;
uxperf.profiler.SensorAjax.DEFAULT_CONFIG[uxperf.profiler.SensorAjax.CONFIG.EVENT_TYPE] = [
	uxperf.profiler.SensorAjax.EventType.BEFORE_SEND,
	uxperf.profiler.SensorAjax.EventType.COMPLETE
];

/**
 * Buffer Item related to ajax events
 *
 * @param {!goog.events.Event} event event object for ajax event
 * @param {!uxperf.profiler.SensorAjax} sensor source of data
 * 
 * @constructor
 * @extends {uxperf.BufferItemSensor}
 */
uxperf.BufferItemAjax = function(event, sensor) {
	goog.base(this, sensor);
	
  /**
   * event object for ajax event
   * @type {goog.events.Event}
   */
	this.event_ = event;
	if (!this.event_.timeStamp) {
		this.event_.timeStamp = goog.now();
		if (goog.DEBUG) LOG.info('timestamp updated manually - no original value in event: ' + this.event_.timeStamp);
	}

	/**
	 * Url path serialization flag
	 * @type {boolean}
	 */
	this.withUrlPath_ = /** @type {boolean} */ sensor.getParam(uxperf.profiler.SensorAjax.CONFIG.WITH_URL_PATH) || false;

	/**
	 * Data serialization flag
	 * @type {boolean}
	 */
	this.withData_ = /** @type {boolean} */ sensor.getParam(uxperf.profiler.SensorAjax.CONFIG.WITH_DATA) || false;
	
	/**
	 * Data serialization flag
	 * @type {boolean}
	 */
	this.withParams_ = /** @type {boolean} */ !!sensor.getParam(uxperf.profiler.SensorAjax.CONFIG.WITH_PARAMS);
	

	/**
	 * Response serialization flag
	 * @type {boolean}
	 */
	this.withResponse_ = /** @type {boolean} */ sensor.getParam(uxperf.profiler.SensorAjax.CONFIG.WITH_RESPONSE) || false;

	/**
	 * Status text serialization flag
	 * @type {boolean}
	 */
	this.withStatusText_ = /** @type {boolean} */ sensor.getParam(uxperf.profiler.SensorAjax.CONFIG.WITH_STATUS_TEXT) || false;
};
goog.inherits(uxperf.BufferItemAjax, uxperf.BufferItemSensor);

/** @inheritDoc */
uxperf.BufferItemAjax.prototype.serialize = function() {
  if (goog.DEBUG) LOG.info('BufferItemAjax - serialize');
	
	/** @type {Object} */
  var res = uxperf.BufferItemAjax.superClass_.serialize.call(this);
	
  res['event_type'] = this.event_.type;
  res['timestamp'] = this.event_.timeStamp;
	
	/** @type {uxperf.profiler.SensorAjax.XMLHttpRequest} */
	var xhr = /** @type {uxperf.profiler.SensorAjax.XMLHttpRequest} */ this.event_.target;
	if (!xhr) return res;
	
	res['xhrID'] = xhr.id_;
	if (xhr['status']) res['status'] = xhr['status'];
	if (xhr['responseType']) res['responseType'] = xhr['responseType'];
	if (this.withUrlPath_) res['urlPath'] = xhr['urlPath_'];
	if (this.withData_) res['data'] = xhr['data_'];
	if (this.withParams_) res['params'] = xhr['params_'];
	if (this.withStatusText_ && xhr['statusText']) res['statusText'] = xhr['statusText'];
	if (this.withResponse_ && xhr['responseText']) res['responseText'] = xhr['responseText'];
	//if (xhr['response']) res['response'] = xhr['response'];
	//if (xhr['responseXML']) res['responseXML'] = xhr['responseXML'];

	return res;
};

/**
 * Event target interface for XMLHttpRequests
 * @implements {EventTarget}
 * @extends {goog.events.EventTarget}
 * @constructor
 */
uxperf.profiler.SensorAjax.XMLHttpRequestEventTarget = function() {
	goog.base(this);
};
goog.inherits(uxperf.profiler.SensorAjax.XMLHttpRequestEventTarget, goog.events.EventTarget);

/**
 * Event types for standard XHR events related to communication.
 * @enum {string}
 */
uxperf.profiler.SensorAjax.XMLHttpRequestEventTarget.EventType = {
	LOADSTART: 'loadstart',
	PROGRESS: 'progress',
	ABORT: 'abort',
	ERROR: 'error',
	LOAD: 'load',
	TIMEOUT: 'timeout',
	LOADEND: 'loadend'
};

/**
 * Proxy class for real XMLHttpRequest objects
 * used to trace activity
 *
 * @param {Object} opt_real real xhr object to use in proxy
 * @extends {uxperf.profiler.SensorAjax.XMLHttpRequestEventTarget}
 * @constructor
 */
uxperf.profiler.SensorAjax.XMLHttpRequest = function(opt_real) {
	goog.base(this);
	
	/**
	 * real instance of XMLHttpRequest represented by this proxy
	 * @type {Object}
	 */
	this.real_ = null;
	
	if (goog.isDefAndNotNull(opt_real)) {
		this.real_ = opt_real;
	} else {
		this.real_ = new uxperf.profiler.SensorAjax.originalXMLHttpRequest_();
		if (!this.real_ && typeof XMLHttpRequest == 'undefined' && typeof ActiveXObject != 'undefined') {
			var ACTIVE_X_IDENTS = ['MSXML2.XMLHTTP.6.0', 'MSXML2.XMLHTTP.3.0',
                             'MSXML2.XMLHTTP', 'Microsoft.XMLHTTP'];
			for (var i = 0; i < ACTIVE_X_IDENTS.length; i++) {
				/** @preserveTry */
				try {
					var ax = new ActiveXObject(ACTIVE_X_IDENTS[i]);
					this.real_ = ax;
					break;
				} catch (e) {
					//
				}
			}
			if (!this.real_) throw Error('XMLHttpRequest not supported');
		}
	}
	
	/**
	 * @type {Object}
	 */
	var real = this.real_;
	
	/**
	 * @type {uxperf.profiler.SensorAjax.XMLHttpRequest}
	 */
	var proxy = this;
	
	/**
	 * flag if define property method is supported
	 * @type {boolean}
	 */
	this.definePropertySupported_ = false;
	
	//test if Object.defineProperty method is supported for custom objects and if it will allow to replace fallback properties
	/** @type {number} */
	this['pV'] = -1;
	/** @type {number} */
	this['p_'] = 0;
	/** @preserveTry */
	try {
		Object.defineProperty(this, 'p_', {
			'set': function(newValue) { proxy['pV'] = newValue; },
			'get': function() { return proxy['pV']; },
			'enumerable': true,
			'configurable': true
		});
		this['p_'] = 2;
		this.definePropertySupported_ = (this['p_'] === this['pV']);
	} catch (e) {
		if (goog.DEBUG) LOG.info('Object.defineProperty method test raised (catched) exception : ' + e.getMessage());
		//
	}
	delete this['pV'];
	delete this['p_'];
	if (goog.DEBUG && !this.definePropertySupported_) LOG.info('Object.defineProperty method is not supported for custom objects fallback solution for updating properties will be in use.');
	
	/**
	 * flag for marking internal library requests
	 * @type {boolean}
	 */
	this.uxInternalRequest_ = false;
	
	/**
	 * content type assigned to request
	 * @type {string}
	 */
	this.contentType_ = '';

	/**
	 * http method used in request
	 * @type {string}
	 */
	this['method_'] = '';

	/**
	 * External generated part of request id
	 * @type {string|Function}
	 */
	this.reqId_ = /** @type {string|Function} */ uxperf.profiler.SensorAjax.getParam(uxperf.profiler.SensorAjax.CONFIG.EXT_REQ_ID) || '_';

	/**
	 * request id
	 * @type {string|undefined}
	 */
	this.id_ = undefined;

	/**
	 * flag if trace headers should be send with each requests
	 * @type {boolean}
	 */
	this.traceHeaders_ = /** @type {boolean} */ uxperf.profiler.SensorAjax.getParam(uxperf.profiler.SensorAjax.CONFIG.TRACE_HEADERS) || true;

	/**
	 * flag if url path should be stored with info about each request
	 * @type {boolean}
	 */
	this.withUrlPath_ = /** @type {boolean} */ uxperf.profiler.SensorAjax.getParam(uxperf.profiler.SensorAjax.CONFIG.WITH_URL_PATH) || false;

	/**
	 * flag if url path should be provided in proxy object (ex. for id calculation)
	 * @type {boolean}
	 */
	this.useUrlPath_ = this.withUrlPath_ || /** @type {boolean} */ uxperf.profiler.SensorAjax.getParam(uxperf.profiler.SensorAjax.CONFIG.USE_URL_PATH) || false;

	/**
	 * base url path for use in calculation of relative url paths
	 * @type {string|undefined}
	 */
	this.baseUrlPath_  = undefined;
	if (this.useUrlPath_) this.baseUrlPath_ = location.protocol + '//' + location.host + '/' + location.pathname;
	
	/**
	 * calculated url path placeholder
	 * @type {string|undefined}
	 */
	this['urlPath_'] = undefined;
	
	/**
	 * list of request POST data should be stored with info about each request
	 * @type {Array.<string>|boolean|null}
	 */
	this.withData_ = /** @type {Array.<string>|boolean|null} */ uxperf.profiler.SensorAjax.getParam(uxperf.profiler.SensorAjax.CONFIG.WITH_DATA) || false;
	
	/**
	 * placeholder for request data
	 * @type {Object|string}
	 */
	this['data_'] = null;
	
	/**
	 * list of request GET params which should be stored with info about each request
	 * if true, all params will be stored, if false - none
	 * @type {Array.<string>|boolean|null}
	 */
	this.withParams_ = /** @type {Array.<string>|boolean|null} */ uxperf.profiler.SensorAjax.getParam(uxperf.profiler.SensorAjax.CONFIG.WITH_PARAMS) || false;
	
	/**
	 * placeholder for request params to report
	 * @type {Object}
	 */
	this['params_'] = null;

	/**
	 * event types to report from configuration
	 * @type {*}
	 */
	var sensorEvents = uxperf.profiler.SensorAjax.getParam(uxperf.profiler.SensorAjax.CONFIG.EVENT_TYPE);

	/**
	 * array of event types to report
	 * @type {Array.<string>}
	 */
	var sensorEventsArr = /** @type {Array.<string>} */ (goog.isArrayLike(sensorEvents) ? goog.array.toArray(/** @type {goog.array.ArrayLike} */ (sensorEvents)) : (goog.isString(sensorEvents) ? [sensorEvents] : []));
	
	/**
	 * flag if event 'before_send' should be reported
	 * @type {boolean}
	 */
	this.reportBeforeSend_ = goog.array.contains(sensorEventsArr, uxperf.profiler.SensorAjax.EventType.BEFORE_SEND);
	/**
	 * flag if event 'complete' should be reported
	 * @type {boolean}
	 */
	this.reportComplete_ = goog.array.contains(sensorEventsArr, uxperf.profiler.SensorAjax.EventType.COMPLETE);
	/**
	 * flag if event 'success' should be reported
	 * @type {boolean}
	 */
	this.reportSuccess_ = goog.array.contains(sensorEventsArr, uxperf.profiler.SensorAjax.EventType.SUCCESS);
	/**
	 * flag if event 'abort' should be reported
	 * @type {boolean}
	 */
	this.reportAbort_ = goog.array.contains(sensorEventsArr, uxperf.profiler.SensorAjax.EventType.ABORT);
	/**
	 * flag if event 'error' should be reported
	 * @type {boolean}
	 */
	this.reportError_ = goog.array.contains(sensorEventsArr, uxperf.profiler.SensorAjax.EventType.ERROR);
	/**
	 * flag if event 'error_upload' should be reported
	 * @type {boolean}
	 */
	this.reportErrorUpload_ = goog.array.contains(sensorEventsArr, uxperf.profiler.SensorAjax.EventType.ERROR_UPLOAD);
	/**
	 * flag if event 'timeout' should be reported
	 * @type {boolean}
	 */
	this.reportTimeout_ = goog.array.contains(sensorEventsArr, uxperf.profiler.SensorAjax.EventType.TIMEOUT);
	/**
	 * flag if event 'timeout_upload' should be reported
	 * @type {boolean}
	 */
	this.reportTimeoutUpload_ = goog.array.contains(sensorEventsArr, uxperf.profiler.SensorAjax.EventType.TIMEOUT_UPLOAD);

	/**
	 * @type {uxperf.profiler.SensorAjax}
	 */
	var sensor = uxperf.profiler.SensorAjax.getInstance();
	//register xhr to report events to sensor (if sensor is available)
	if (sensor && (sensorEventsArr.length > 0)) goog.events.listen(this, sensorEventsArr, sensor);
	
	//event handling
	
	/**
	 * Event types provided by XMLHttpRequest
	 * @type {Array.<string>}
	 */
	this.evtypes = goog.object.getValues(uxperf.profiler.SensorAjax.XMLHttpRequestEventTarget.EventType);
	goog.array.extend(this.evtypes, goog.object.getValues(uxperf.profiler.SensorAjax.XMLHttpRequest.EventType));

	goog.array.forEach(this.evtypes, function (evtype) {
		//if (goog.DEBUG) LOG.info('register on' + evtype + ' handler.');
		proxy['on' + evtype] = goog.nullFunction;
		//diagnostic option
		//real['on' + evtype] = function (event) {
			//if (goog.DEBUG) LOG.info('on' + evtype + ' - start');
			//proxy.handleEvent.call(proxy, event);
			//if (goog.DEBUG) LOG.info('on' + evtype + ' - finished');
		//}
	});
	goog.events.listen(/** @type {EventTarget} */ (this.real_), this.evtypes, this.handleEvent, false, this);

	//states
	/**
	 * @type {number}
	 */
  this['UNSENT'] = this.real_['UNSENT'];
	/**
	 * @type {number}
	 */
  this['OPENED'] = this.real_['OPENED'];
	/**
	 * @type {number}
	 */
  this['HEADERS_RECEIVED'] = this.real_['HEADERS_RECEIVED'];
	/**
	 * @type {number}
	 */
  this['LOADING'] = this.real_['LOADING'];
	/**
	 * @type {number}
	 */
  this['DONE'] = this.real_['DONE'];
	
	/**
	 * @type {number}
	 */
	this['readyState'] = this['UNSENT'];
	if (this.definePropertySupported_) {
		Object.defineProperty(this, 'readyState', {
			'get': function () { return real['readyState']; },
			'set': function (newValue) { real['readyState'] = newValue; },
			'enumerable': true,
			'configurable': false
		});
	}

	//request related methods and properties
	
	/**
	 * flag if XHR was opened in synchronous mode
	 * @type {boolean}
	 */
	this.synchronous_ = false;
	//TODO implement Object.defineProperty version
	
	/**
	 * open method of XHR object
	 * 
	 * @param {string} method
	 * @param {string} url
	 * @param {boolean=} opt_async
	 * @param {string=} opt_username
	 * @param {string=} opt_password
	 */
	this['open'] = function (method, url, opt_async, opt_username, opt_password) {
		//if (goog.DEBUG) LOG.info('XHR proxy - open - start');
		
		proxy.send_ = false;
		proxy.synchronous_ = !opt_async;
		proxy.uxInternalRequest_ = false;
		proxy.updateBefore();
		proxy['method_'] = method ? method.toLowerCase() : method;

		/** @type {goog.Uri} */
		var uri = null;
		
		/**
		 * @type {number}
		 */
		var pos = -2;
		if (proxy.useUrlPath_) {
			//TODO: verify if there will be any optimisation if we will use goog.Uri instead of string parsing
			//uri = uri || new goog.Uri(url);
			var posH = url.indexOf('#');
			var posQ = url.indexOf('?');
			pos = posH < 0 ? posQ : (posQ < 0 ? posH : Math.min(posQ, posH));
			proxy['urlPath_'] = pos > 0 ? url.substr(0, pos) : (pos == 0 ? proxy.baseUrlPath_ : url);
		}
		if (proxy.withParams_) {
			/** @type {Array|null} */
			var params = null;
			if (proxy.withParams_ === true) {
				uri = uri || new goog.Uri(url);
				/** @type {goog.Uri.QueryData} */
				var qd = uri.getQueryData();
				params = qd ? qd.getKeys() : null;
			} else if (goog.isArrayLike(proxy.withParams_)) {
				if (proxy.withParams_.length > 0)
					params = /** @type {Array} */ proxy.withParams_;
			}
			if (params) {
				uri = uri || new goog.Uri(url);
				proxy['params_'] = {};
				for (var i=0; i<params.length; i++) {
					/** @type {Array|string|null} */
					var v = uri.getParameterValues(params[i]);
					if (v && (v.length > 0)) {
						if (v.length == 1) v = v[0];
					} else {
						v = '';
					}
					proxy['params_'][params[i]] = v;
				}
			}
		}
		//if (goog.DEBUG) LOG.info('XHR proxy - open - before real');
		return real['open'].call(real, method, url, opt_async, opt_username, opt_password);
	};
	
	/**
	 * set request header method of XHR object
	 * with detection of headers specific to uxperf library
	 *
	 * @param {string} header
	 * @param {string|null} value
	 */
	this['setRequestHeader'] = function (header, value) {
//		if (goog.DEBUG) LOG.info('XHR proxy - setRequestHeader - start');
		real['setRequestHeader'].call(real, header, value);
		if (!proxy.uxInternalRequest_ && (header.substr(0, uxperf.profiler.SensorAjax.X_UXPERF_HEADER_PREFIX.length) == uxperf.profiler.SensorAjax.X_UXPERF_HEADER_PREFIX)) proxy.uxInternalRequest_ = true;
		if (header.toLowerCase() == 'content-type') proxy.contentType_ = value || '';
	};

	/**
	 * timeout property of XHR
	 * @type {number}
	 */
	this['timeout'] = 0;
	//TODO implement Object.defineProperty version
	
	/**
	 * previous value of timeout property
	 * used to detect need for update
	 * @type {number}
	 */
	this.timeoutOld_ = this['timeout'];
	
	/**
	 * flag if reusing credentials is allowed for XHR
	 * @type {boolean}
	 */
	this['withCredentials'] = false;
	//TODO implement Object.defineProperty version
	
	/**
	 * previous value of withCredentials property
	 * used to detect need for update
	 * @type {boolean}
	 */
	this.withCredentialsOld_ = this['withCredentials'];
	
	/**
	 * placeholder for real instance of upload event target
	 * @type {Object}
	 */
	this['upload_real_'] = null;
	
	/**
	 * upload proxy object instance
	 * @type {uxperf.profiler.SensorAjax.XMLHttpRequestEventTarget}
	 */
	this['upload'] = null;
//if (goog.DEBUG) LOG.info('initial updateUpload - before');
	this.updateUpload();
//if (goog.DEBUG) LOG.info('initial updateUpload - after');
	
	/**
	 * flag if send method was called on this instance
	 * @type {boolean}
	 */
	this.send_ = false;
		
	/**
	 * send method of XHR object
	 * @param {ArrayBuffer|Blob|Document|string|FormData} data
	 */
	this['send'] = function (data) {
		if (goog.DEBUG) LOG.info('XHR proxy - send - start');
		proxy.updateBefore();
		if (!proxy.uxInternalRequest_) {
			if ((proxy['method_'] == 'post') && data && proxy.withData_) {
				var dt = data;
				if (proxy.withData_ === true) {
					proxy['data_'] = dt;
				} else if (goog.isArrayLike(proxy.withData_) && (proxy.withData_.length > 0)) {
					proxy['data_'] = {};
					
					if (goog.isObject(dt)) {
						if (dt instanceof Document) {
							for (var i = 0; i < proxy.withData_.length; i++) {
								/** @type {Element} */
								var el = dt.getElementById(proxy.withData_[i]);
								if (el)	proxy['data_'][proxy.withData_[i]] = el.textContent ? el.textContent : (el.value ? el.value : '?');
							}
						} else if (dt instanceof FormData) {
							//[Constructor, Constructor(HTMLFormElement form)]
              //interface FormData {
              //  void append(DOMString name, Blob value, optional DOMString filename);
							//  void append(DOMString name, DOMString value);
							//};
							
							proxy['data_'][uxperf.profiler.SensorAjax.ERROR_KEY] = 'Unsupported data type - FormData';
						} else if (dt instanceof ArrayBuffer) {
						  //var bb = new BlobBuilder();
							//bb.append(dt);
							//var f = new FileReader();
							//f.onload = function(e) {
							//	dt = e.target.result;
							//}
							//f.readAsText(bb.getBlob());
							
							proxy['data_'][uxperf.profiler.SensorAjax.ERROR_KEY] = 'Unsupported data type - ArrayBuffer';
						} else if (dt instanceof Blob) {
							//var f = new FileReader();
							//f.onload = function(e) {
							//  dt = e.target.result;
							//}
							//f.readAsText(dt);
							
							proxy['data_'][uxperf.profiler.SensorAjax.ERROR_KEY] = 'Unsupported data type - Blob';
						}
					} else if (!goog.isString(proxy.contentType_) || (proxy.contentType_ == '') || (proxy.contentType_.toLowerCase() == 'application/x-www-form-urlencoded')) {
						try {
							/** @type {goog.Uri.QueryData} */
							var qd = new goog.Uri.QueryData(/** @type {string} */ (dt));
							for (var i = 0; i < proxy.withData_.length; i++) {
								/** @type {Array|string|number|null} */
								var v = qd.getValues(proxy.withData_[i]);
								if (v.length == 0) continue;
								if (v.length == 1) v = v[0];
								proxy['data_'][proxy.withData_[i]] = v;
							}
						} catch (e) {
							proxy['data_'][uxperf.profiler.SensorAjax.ERROR_KEY] = 'Can not parse data';
						}
					} else {
						proxy['data_'][uxperf.profiler.SensorAjax.ERROR_KEY] = 'Unsupported content type: ' + proxy.contentType_;
					}
				}
			}

			/**
			 * @type {Array.<string>}
			 */
			var unId = ['' + goog.now()];
			if (goog.isString(proxy.reqId_)) {
				unId.push(proxy.reqId_);
			} else {
				/** @preserveTry */
				try {
					/**
					 * calculated request id
					 * @type {string}
					 */
					var calculated = /** @type {string} */ proxy.reqId_.call(proxy);
					unId.push(calculated);
				} catch (e) {
					unId.push('Error in ID calculation: ' + (e.getMessage ? e.getMessage() : e));
				}
			}
			unId.push(Math.floor(Math.random() * 2147483648).toString(36));
			proxy.id_ = unId.join('.');
			if (proxy.traceHeaders_) {
				real['setRequestHeader'].call(real, uxperf.profiler.SensorAjax.X_UXPERF_AJAX_ID_HEADER, proxy.id_);
			}
			//if (goog.DEBUG) LOG.info('XHR proxy - send - dispatch event before_send');
			if (proxy.reportBeforeSend_) proxy.dispatchEvent(uxperf.profiler.SensorAjax.EventType.BEFORE_SEND);
		}
		//if (goog.DEBUG) LOG.info('XHR proxy - send - before real');
	  real['send'].call(real, data);
		proxy.send_ = true;
		//if (goog.DEBUG) LOG.info('XHR proxy - send - finished');
	};
	
	/**
	 * abort method of XHR object
	 */
	this['abort'] = function () {
		if (goog.DEBUG) LOG.info('XHR proxy - abort - start');
		real['abort'].call(real);
		proxy.updateAfter();
		proxy.send_ = false;

		if (proxy.reportComplete_) proxy.dispatchEvent(uxperf.profiler.SensorAjax.EventType.COMPLETE);
		if (proxy.reportAbort_) proxy.dispatchEvent(uxperf.profiler.SensorAjax.EventType.ABORT);
		if (goog.DEBUG) LOG.info('XHR proxy - abort - finished');
	};

	//response related methods and properties
	
	/**
	 * response status
	 * @type (number)
	 */
	this['status'] = 0;
	//TODO implement Object.defineProperty version
	
	/**
	 * response status description
	 * @type {string}
	 */
	this['statusText'] = '';
	//TODO implement Object.defineProperty version
	
	/**
	 * method to get response header for XHR object
	 * @param {string} header name
	 * @return {string|null}
	 */
  this['getResponseHeader'] = function (header) {
		//if (goog.DEBUG) LOG.info('XHR proxy - getResponseHeader - header: ' + header);
		return real['getResponseHeader'].call(real, header);
	};
	
	/**
	 * method to get all response headers for XHR object
	 * @return {string|null}
	 */
  this['getAllResponseHeaders'] = function () {
		//if (goog.DEBUG) LOG.info('XHR proxy - getAllResponseHeaders');
		return real['getAllResponseHeaders'].call(real);
	};
	
	if (real['overrideMimeType']) {
		/**
		 * override mime type method of XHR object
		 * @param {string} mime
		 */
	  this['overrideMimeType'] = function (mime) {
			//if (goog.DEBUG) LOG.info('XHR proxy - overrideMimeType with: ' + mime);
			real['overrideMimeType'].call(real, mime);
		};
	}
	
	/**
	 * response type value
	 * @type {string}
	 */
	this['responseType'] = '';
	//TODO implement Object.defineProperty version

	/**
	 * previous value of responseType property
	 * used to detect need for update
	 * @type {string}
	 */
	this.responseTypeOld_ = this['responseType'];
	
	/**
	 * response as is in original XHR object
	 * @type {*}
	 */
	this['response'] = '';
	//TODO implement Object.defineProperty version
	
	/**
	 * response text representation
	 * @type {string}
	 */
	this['responseText'] = '';
	//TODO implement Object.defineProperty version
	
	/**
	 * response XML document if available in original XHR object
	 * @type {Document}
	 */
	this['responseXML'] = null;
	//TODO implement Object.defineProperty version
};
goog.inherits(uxperf.profiler.SensorAjax.XMLHttpRequest, uxperf.profiler.SensorAjax.XMLHttpRequestEventTarget);

/**
 * Disposes proxy and removes all listeners that were registered on real XHR.
 * @override
 * @protected
 */
uxperf.profiler.SensorAjax.XMLHttpRequest.prototype.disposeInternal = function () {
	if (goog.DEBUG) LOG.info('XHR proxy - disposeInternal - start');
	//goog.array.forEach(this.evtypes, function (evtype) {
	//	delete this.real_['on'+evtype];
	//});
	
  uxperf.profiler.SensorAjax.XMLHttpRequest.superClass_.disposeInternal.call(this);
	//unregister this XHR from sensor (if available)
	//var sensor = uxperf.profiler.SensorAjax.getInstance();
	//if (sensor) goog.events.unlisten(this, goog.object.getValues(uxperf.profiler.SensorAjax.EventType), sensor);
	//already done in superClass dispose goog.events.removeAll(this);
	
	if (this.real_) goog.events.removeAll(this.real_);
	if (this.upload) {
		this.upload.dispose();
		delete this.upload;
	}
	/** @preserveTry */
	try {
		this.real_.abort();
	} catch (e) {
		//
	}
	delete this.real_;
	if (this['data_']) delete this['data_'];
	if (this['urlPath_']) delete this['urlPath_'];
	if (goog.DEBUG) LOG.info('XHR proxy - disposeInternal - finished');
};

/**
 * update original XHR object properties before execution of some method on it
 * @private
 */
uxperf.profiler.SensorAjax.XMLHttpRequest.prototype.updateBefore = function() {
	//if (goog.DEBUG) LOG.info('XHR proxy - updateBefore - start');

	if (this['timeout'] !== this.timeoutOld_) {
		this.real_['timeout'] = this['timeout'];
		this.timeoutOld_ = this['timeout'];
		//if (goog.DEBUG) LOG.info('XHR proxy - updateBefore - after timeout set');
	}
	
	if ((this['withCredentials'] !== this.withCredentialsOld_) && !this.send_ && ((this['readyState'] == this['UNSENT']) || (this['readyState'] == this['OPENED']))) {
		this.real_['withCredentials'] = this['withCredentials'];
		this.withCredentialsOld_ = this['withCredentials'];
		//if (goog.DEBUG) LOG.info('XHR proxy - updateBefore - after withCredentials');
	}
	
	if ((this['responseType'] !== this.responseTypeOld_) && (this['readyState'] != this['LOADING']) && (this['readyState'] != this['DONE']) && !this.synchronous_) {
		this.real_['responseType'] = this['responseType'];
		this.responseTypeOld_ = this['responseType'];
		//if (goog.DEBUG) LOG.info('XHR proxy - updateBefore - after responseType');
	}
	
	//if (goog.DEBUG) LOG.info('XHR proxy - updateBefore - finished');
};

/**
 * update upload property of proxy based on value of original XHR object
 * @private
 */
uxperf.profiler.SensorAjax.XMLHttpRequest.prototype.updateUpload = function() {
	//if (goog.DEBUG) LOG.info('XHR proxy - updateUpload - start');
	if (this.real_['upload']) {
		//if (goog.DEBUG) LOG.info('XHR proxy - updateUpload - there is upload in real XHR');
		if (!this.upload_real_ || (this.real_['upload'] != this.upload_real_)) {
			//if (goog.DEBUG) LOG.info('XHR proxy - updateUpload - this is new upload');
			if (this['upload']) {
				//if (goog.DEBUG) LOG.info('XHR proxy - updateUpload - there was upload defined in proxy before');
				this['upload'].dispose();
				this['upload'] = null;
			}
			this['upload'] = new uxperf.profiler.SensorAjax.XMLHttpRequestEventTarget(); //upload event reporting
			//TODO: verify if listening for events is enough in all browsers or we shoud setup on* functions
			/** @type {Array} */
			var eventTypes = goog.object.getValues(uxperf.profiler.SensorAjax.XMLHttpRequestEventTarget.EventType);
			goog.events.listen(this.real_['upload'], eventTypes, this.handleUpEvent, false, this);
			/** @type {uxperf.profiler.SensorAjax.XMLHttpRequest} */
			var proxy = this;
			goog.array.forEach(eventTypes, function (evtype) {
				//if (goog.DEBUG) LOG.info('register on' + evtype + ' handler on upload.');
				proxy['upload']['on' + evtype] = goog.nullFunction;
			});

			this.upload_real_ = this.real_['upload'];
		}
	}
	//if (goog.DEBUG) LOG.info('XHR proxy - updateUpload - finished');
};

/**
 * update proxy properties with values from original XHR object
 * after execution of some method on it or before handling event
 * @private
 */
uxperf.profiler.SensorAjax.XMLHttpRequest.prototype.updateAfter = function() {
	//if (goog.DEBUG) LOG.info('XHR proxy - updateAfter - start');
  this['readyState'] = this.real_['readyState'] || this['UNSENT'];
	//if (goog.DEBUG) LOG.info('XHR proxy - updateAfter - after readyState get');
  this['timeout'] = this.real_['timeout'] || 0;
	//if (goog.DEBUG) LOG.info('XHR proxy - updateAfter - after timeout get');
	this['withCredentials'] = this.real_['withCredentials'] || false;
	//if (goog.DEBUG) LOG.info('XHR proxy - updateAfter - after withCredentials get');
	this.updateUpload();
	if (this['readyState']==this['DONE']) {
		this['status'] = this.real_['status'] || undefined;
		//if (goog.DEBUG) LOG.info('XHR proxy - updateAfter - after status get');
		this['statusText'] = this.real_['statusText'] || undefined;
		//if (goog.DEBUG) LOG.info('XHR proxy - updateAfter - after statusText get : ' + this['statusText']);
		this['responseType'] = this.real_['responseType'];
		//if (goog.DEBUG) LOG.info('XHR proxy - updateAfter - after responseType get : ' + this['responseType']);
		this['response'] = this.real_['response'] || undefined;
		//if (goog.DEBUG) LOG.info('XHR proxy - updateAfter - after response get');
		//response text można odczytywać tylko jeżeli responseType jest równe 'text' lub ''
		this['responseText'] = ((this['responseType'] == '') || (this['responseType'] == 'text')) ? this.real_['responseText'] : '';
		//if (goog.DEBUG) LOG.info('XHR proxy - updateAfter - after responseText get: ' + this['responseText']);
		//response xml można odczytać tylko jeżeli responseType jest równe 'document' lub ''
		this['responseXML'] = ((this['responseType'] == '') || (this['responseType'] == 'document')) ? this.real_['responseXML'] : undefined;
	}
	//if (goog.DEBUG) LOG.info('XHR proxy - updateAfter - finished');
};

/**
 * upload XHR event handler
 * @param {Event} event
 */
uxperf.profiler.SensorAjax.XMLHttpRequest.prototype.handleUpEvent = function(event) {
	//if (goog.DEBUG) LOG.info('XHR proxy - handleUpEvent - start - event type: ' + event.type);
	if (!this['upload']) return;
	
	if (!this.uxInternalRequest_) {
		//if (goog.DEBUG) LOG.info('XHR proxy - handleUpEvent - not internal uxperf request');
		if (this.reportTimeoutUpload_ && (event.type == uxperf.profiler.SensorAjax.XMLHttpRequestEventTarget.EventType.TIMEOUT)) {
			this.dispatchEvent(uxperf.profiler.SensorAjax.EventType.TIMEOUT_UPLOAD);
		} else if (this.reportErrorUpload_ && (event.type == uxperf.profiler.SensorAjax.XMLHttpRequestEventTarget.EventType.ERROR)) {
			this.dispatchEvent(uxperf.profiler.SensorAjax.EventType.ERROR_UPLOAD);
		}
	}
	
	event.target = this['upload'];
	
	//if (goog.DEBUG) LOG.info('XHR proxy - handleUpEvent - call handler for type: ' + event.type);
	if (this['upload']['on' + event.type])
		this['upload']['on' + event.type].call(this['upload'], event);
	
	//if (goog.DEBUG) LOG.info('XHR proxy - handleUpEvent - dispatch event');
	this['upload'].dispatchEvent(event);
	
	//if (goog.DEBUG) LOG.info('XHR proxy - handleUpEvent - finished');
};

/**
 * main XHR event handler
 * @param {Event} event
 */
uxperf.profiler.SensorAjax.XMLHttpRequest.prototype.handleEvent = function(event) {
	//if (goog.DEBUG) LOG.info('XHR proxy - handleEvent - start - event type: ' + event.type);
	this.updateAfter();
	
	if (!this.uxInternalRequest_) {
		//if (goog.DEBUG) LOG.info('XHR proxy - handleEvent - not internal uxperf request');
		if (event.type == uxperf.profiler.SensorAjax.XMLHttpRequestEventTarget.EventType.TIMEOUT) {
			if (this.reportTimeout_) this.dispatchEvent(uxperf.profiler.SensorAjax.EventType.TIMEOUT);
			this.callHandlers(event);
			return;
		}
		
		if (event.type != uxperf.profiler.SensorAjax.XMLHttpRequest.EventType.READYSTATECHANGE) {
			this.callHandlers(event);
			return;
		}
		if (this.real_.readyState != this['DONE']) {
			this.callHandlers(event);
			return;
		}
		
		if (this.reportComplete_) this.dispatchEvent(uxperf.profiler.SensorAjax.EventType.COMPLETE);
		
		if (goog.net.HttpStatus.isSuccess(this.real_.status)) {
			if (this.reportSuccess_) this.dispatchEvent(uxperf.profiler.SensorAjax.EventType.SUCCESS);
		} else {
			if (this.reportError_) this.dispatchEvent(uxperf.profiler.SensorAjax.EventType.ERROR);
		}
		this.cleanup();
	} else if ((event.type == uxperf.profiler.SensorAjax.XMLHttpRequest.EventType.READYSTATECHANGE) &&
		         (this.real_.readyState == this['DONE'])) {
		this.cleanup();
	}
	
	this.callHandlers(event);
	if (goog.DEBUG) LOG.info('XHR proxy - handleEvent - finished');
};

/**
 * cleanup after abort or done
 */
uxperf.profiler.SensorAjax.XMLHttpRequest.prototype.cleanup = function() {
	//if (goog.DEBUG) LOG.info('XHR proxy - cleanup - start');
	if (this['data_'] && this['data_']['body']) this['data_']['body'] = null;
};

/**
 * call assigned event handlers
 * @param {Event} event
 */
uxperf.profiler.SensorAjax.XMLHttpRequest.prototype.callHandlers = function(event) {
	//if (goog.DEBUG) LOG.info('XHR proxy - callHandlers - start - event type: ' + event.type);
	event.target = this;
	
	if (this['on' + event.type])
		this['on' + event.type].call(this, event);
	
	//if (goog.DEBUG) LOG.info('XHR proxy - callHandlers - dispatch');
	this.dispatchEvent(event);
	
	//if (goog.DEBUG) LOG.info('XHR proxy - callHandlers - finished');
};

/**
 * Event type for standard XHR event related to states.
 * @enum {string}
 */
uxperf.profiler.SensorAjax.XMLHttpRequest.EventType = {
	READYSTATECHANGE : 'readystatechange'
};

/**
 * Proxy class for real XDomainRequest objects
 * used to trace activity
 *
 * @extends {uxperf.profiler.SensorAjax.XMLHttpRequest}
 * @constructor
 */
uxperf.profiler.SensorAjax.XDomainRequest = function() {
	goog.base(this, new uxperf.profiler.SensorAjax.XDomainRequest());
};
goog.inherits(uxperf.profiler.SensorAjax.XDomainRequest, uxperf.profiler.SensorAjax.XMLHttpRequest);
